package client

import (
	"crypto/tls"
	"github.com/akirakazu/go-simplejson"
	"github.com/gorilla/websocket"
	"github.com/nu7hatch/gouuid"
	"sync"
	"time"
)

// Message type through send channel
type WriteStruct struct {
	Data []byte
	T    int
}

type Client struct {
	identity        *json.Json
	pwd             string
	binMeta         *json.Json
	connectStatus   *sync.Mutex
	connected       bool
	ready           bool
	queue           []*pack
	conn            *websocket.Conn
	send            chan *WriteStruct
	defaultConnIP   string
	defaultConnPort int
	defaultCb       func(string, *json.Json, *json.Json)
	waiters         map[string]*waiter
	listeners       map[string]*listener
	fileSenders     map[string]*fileSender
	fileRecvs       map[string]*fileRecv
	delegate        *ClientDelegate
}

func NewClient(identity *json.Json, pwd string, cd *ClientDelegate) {
	client := new(Client)
	if !identity.Has("session_id") {
		uid, _ := uuid.NewV4()
		identity.Set("session_id", uid.String())
	}
	client.identity = identity
	client.pwd = pwd
	client.delegate = cd
	client.connectStatus = &sync.Mutex{}
	client.connected = false
	client.ready = false
	client.send = make(chan *WriteStruct, 1000)
	client.defaultConnIP = "conn.vlings.net"
	client.defaultConnPort = 443
	client.defaultCb = func(id string, req *json.Json, res *json.Json) {
		if res.Has("err") {
			client.delegate.onError(res.Get("err"))
		}
	}
	go client.connect()
	return client
}

func (c *Client) connect() {
	c.connectStatus.Lock()
	if c.connected {
		p.connectStatus.Unlock()
		return
	}
	url := "wss://" + c.defaultConnIP + ":" + c.defaultConnPort + "/"
	cc := &websocket.Dialer{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
	conn, _, err := cc.Dial(url, nil)
	if err != nil {
		c.connectStatus.Unlock()
		c.retry()
		return
	}
	conn.SetReadDeadline(time.Now().Add(150 * time.Second))
	conn.SetWriteDeadline(time.Now().Add(150 * time.Second))

	// SetPinghandler
	conn.SetPingHandler(func(data string) error {
		conn.WriteControl(websocket.PongMessage, []byte(""), time.Now().Add(time.Second*10))
		conn.SetReadDeadline(time.Now().Add(150 * time.Second))
		conn.SetWriteDeadline(time.Now().Add(150 * time.Second))
		return nil
	})

	c.connected = true
	c.conn = conn
	go c.readPump()
	go c.writePump()
	c.connectStatus.Unlock()
	c.login()
}

func (c *Client) readPump() {
	for {
		if !c.connected || c.conn == nil {
			continue
		}
		t, data, err := c.conn.ReadMessage()
		if err != nil {
			c.retry()
			return
		}

		if t == websocket.TextMessage {
			m, err := Validate(string(data))
			if err != nil {
				continue
			}
			c.onTextMessage(m)
		} else if t == websocket.BinaryMessage {
			c.onBinMessage(data)
		}
	}
}

func (c *Client) writePump() {
	for {
		if !c.connected || c.conn == nil {
			continue
		}
		message := <-c.send
		err := c.conn.WriteMessage(message.T, message.Data)
		if err != nil {
			c.retry()
			return
		}
	}
}

func (c *Client) onTextMessage(m *Pack) {
	cmd := m.Cmd
	if cmd == "request" {
		c.onRequest(m)
	} else if cmd == "response" {
		c.onResponse(m)
	} else if cmd == "start_binary" {
		c.onStartBinary(m)
	} else if cmd == "end_binary" {
		c.onEndBinary(m)
	} else if cmd == "push" {
		c.onPush(m)
	} else if cmd == "event" {
		c.onEvent(m)
	} else if cmd == "redirect" {
		c.onRedirect(m)
	} else if cmd == "binary" {
		c.onBinary(m)
	}
}

func (c *Client) onBinMessage(data []byte) {
	if c.binMeta == nil {
		return
	}
	id := c.binMeta.id
	fileRecv, ok := c.fileRecvs[id]
	if ok {
		fileRecv.write(data)
	} else {
		c.delegate.onByteArray(c.binMeta, data)
	}

	c.binMeta = nil
}

func (c *Client) retry() {
	c.connected = false
	c.ready = false
	if c.conn != nil {
		c.conn.Close()
	}
	c.conn = nil
	time.Sleep(5 * time.Second)
	c.connect()
}

func (c *Client) sendPack(m *Pack, shouldQueue bool) bool {
	if !c.connected {
		if shouldQueue {
			c.queue = append(c.queue, m)
		}
		return false
	}
	if m.Cmd != "login" && !c.ready {
		return false
	}

	json, err := m.MarshalJSON()
	if err != nil {
		return false
	}
	w := &WriteStruct{T: websocket.TextMessage, Data: json}
	c.send <- w
	return true
}

func (c *Client) sendBinary(b []byte) bool {
	if !c.ready {
		return false
	}
	w := &WriteStruct{T: websocket.BinaryMessage, Data: b}
	c.send <- w
	return true
}

func (c *Client) addRequest(id string, m *Pack, callback func(string, *json.Json, *json.Json), addTimeout bool) {
	w := newWaiter(m.Msg, 20*time.Second, callback)
	if addTimeout {
		w.start()
	}
	c.waiters[id] = w
}
